﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.ServiceModel.Description;
using CtefCrmImport.Common.Interfaces;
using CtefCrmImport.Common.Extensions;
using CtefCrmImport.Common.Models;
using Microsoft.Crm.Sdk;
using Microsoft.Crm.Sdk.Messages;
using Microsoft.Crm.Sdk.Samples;
using Microsoft.Crm.Services.Utility;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Client;
using Microsoft.Xrm.Sdk.Query;
using Microsoft.Xrm.Sdk.Messages;

namespace CtefCrmImport.Common.Operations
{
    public class CommonOperations
    {
        private const string DummyDonorName = "friend ctef";

        private readonly ILogger logger;
        private Dictionary<string, EntityReferenceCollection> currencyMap;

        public CommonOperations(ILogger logger)
        {
            if (logger == null) throw new ArgumentNullException("logger");

            this.logger = logger;
        }

        public void CheckVersion(IOrganizationService service)
        {
            var versionRequest = new RetrieveVersionRequest();
            var versionResponse =
                (RetrieveVersionResponse)service.Execute(versionRequest);

            Log("Microsoft Dynamics CRM version {0}.", versionResponse.Version);
        }

        public HashSet<string> BuildTransactions(IOrganizationService service)
        {
            var result = new HashSet<string>();
            using (var orgSvcContext = new OrganizationServiceContext(service))
            {
                var donations = orgSvcContext.CreateQuery("new_donation");
                foreach (var d in donations)
                {
                    var id = d.GetAttributeValue<string>("new_transactionid");
                    if (!string.IsNullOrWhiteSpace(id))
                    {
                        result.Add(id.Trim());
                    }
                }
            }

            return result;
        }

        public IDictionary<string, EntityReferenceCollection> BuildFullNameContactMap(IOrganizationService service)
        {
            var result = new Dictionary<string, EntityReferenceCollection>();

            using (var orgSvcContext = new OrganizationServiceContext(service))
            {
                var contacts = orgSvcContext.CreateQuery("contact");
                foreach (var c in contacts)
                {
                    var name = c.GetAttributeValue<string>("fullname");

                    if (string.IsNullOrWhiteSpace(name) || string.Equals(name, DummyDonorName, StringComparison.OrdinalIgnoreCase)) continue;

                    name = name.ToKey(false);
                    var id = c.GetAttributeValue<Guid?>("contactid");

                    if (id.HasValue)
                    {
                        PopulateContactByName(name, id.Value, result);
                    }
                }
            }

            return result;
        }

        public IDictionary<string, EntityReferenceCollection> BuildEmailContactMap(IOrganizationService service)
        {
            var result = new Dictionary<string, EntityReferenceCollection>();

            using (var orgSvcContext = new OrganizationServiceContext(service))
            {
                var contacts = orgSvcContext.CreateQuery("contact");
                foreach (var c in contacts)
                {
                    var email1 = c.GetAttributeValue<string>("emailaddress1");
                    var email2 = c.GetAttributeValue<string>("emailaddress2");
                    var email3 = c.GetAttributeValue<string>("emailaddress3");
                    var id = c.GetAttributeValue<Guid?>("contactid");

                    if (id.HasValue)
                    {
                        PopulateContactByEmail(email1, id.Value, result);
                        PopulateContactByEmail(email2, id.Value, result);
                        PopulateContactByEmail(email3, id.Value, result);
                    }
                }
            }

            return result;
        }

        public void BuildContactMaps(
            IOrganizationService service,
            out IDictionary<string, EntityReferenceCollection> emailContactMap,
            out IDictionary<string, EntityReferenceCollection> fullNameContactMap
            )
        {
            emailContactMap = BuildEmailContactMap(service);
            fullNameContactMap = BuildFullNameContactMap(service);
        }

        public EntityReferenceCollection FindDonor(
            Donor donor,
            IDictionary<string, EntityReferenceCollection> emailContactMap,
            IDictionary<string, EntityReferenceCollection> fullNameContactMap)

        {
            if (donor == null) throw new ArgumentNullException("donor");

            var donorRef = FindContactByEmail(donor.PrimaryEmail, emailContactMap);
            if (donorRef != null)
            {
                Log("Info: found matching email address: {0}", donor.PrimaryEmail);
                return donorRef;
            }

            donorRef = FindContactByEmail(donor.EmailForStudents, emailContactMap);
            if (donorRef != null)
            {
                Log("Info: found matching email address: {0}", donor.EmailForStudents);
                return donorRef;
            }

            // Match by full name if email match failed except for the dummy name
            if (!string.IsNullOrWhiteSpace(donor.FullName) && 
                !string.Equals(donor.FullName.Trim(), DummyDonorName, StringComparison.OrdinalIgnoreCase) &&                
                fullNameContactMap != null)
            {
                var key = donor.FullName.ToKey(false);            
                if (fullNameContactMap.ContainsKey(key))
                {
                    Log("Info: found matching full name: {0}", key);
                    return fullNameContactMap[key];
                }
            }

            return null;
        }

        public EntityReferenceCollection FindStudent(
            Student student,
            IDictionary<string, EntityReferenceCollection> emailContactMap,
            IDictionary<string, EntityReferenceCollection> fullNameContactMap)
        {
            if (student == null) throw new ArgumentNullException("student");

            var studentRef = FindContactByEmail(student.PrimaryEmail, emailContactMap);
            if (studentRef != null)
            {
                Log("Info: found matching email address: {0}", student.PrimaryEmail);
                return studentRef;
            }

            // Match by full name if email match failed
            if (!string.IsNullOrWhiteSpace(student.FullName) && fullNameContactMap != null)
            {
                var key = student.FullName.ToKey(false);
                if (fullNameContactMap.ContainsKey(key))
                {
                    Log("Info: found matching full name: {0}", key);
                    return fullNameContactMap[key];
                }
            }

            return null;
        }

        public EntityReferenceCollection FindContactByEmail(
            string email,
            IDictionary<string, EntityReferenceCollection> emailContactMap)
        {
            if (string.IsNullOrWhiteSpace(email) || emailContactMap.IsNullOrEmpty()) return null;

            var key = email.ToKey(true);
            return emailContactMap.ContainsKey(key) ? emailContactMap[key] : null;
        }

        public void ImportDonors(
            IOrganizationService service, 
            IEnumerable<Donor> donors,
            IDictionary<string, EntityReferenceCollection> emailContactMap,
            IDictionary<string, EntityReferenceCollection> fullNameContactMap)
        {
            // Set contact type as "Donor" using hard-coded option value from CTEF schema definition
            ImportContacts(service, donors, new OptionSetValue(100000001), emailContactMap, fullNameContactMap);
        }

        public void ImportStudents(
            IOrganizationService service,
            IEnumerable<Student> students,
            IDictionary<string, EntityReferenceCollection> emailContactMap,
            IDictionary<string, EntityReferenceCollection> fullNameContactMap)
        {
            // Set contact type as "Student" using hard-coded option value from CTEF schema definition
            ImportContacts(service, students, new OptionSetValue(100000000), emailContactMap, fullNameContactMap);
        }

        public void ImportContacts(
            IOrganizationService service,
            IEnumerable<Donor> contacts,
            OptionSetValue contactType,
            IDictionary<string, EntityReferenceCollection> emailContactMap,
            IDictionary<string, EntityReferenceCollection> fullNameContactMap)
        {
            if (service == null) throw new ArgumentNullException("service");
            if (contacts == null) throw new ArgumentNullException("contacts");
            if (contactType == null) throw new ArgumentNullException("contactType");
            if (emailContactMap == null) throw new ArgumentNullException("emailContactMap");

            int processedCount = 0;
            int createdCount = 0;

            foreach (var d in contacts)
            {
                processedCount++;

                var match = FindDonor(d, emailContactMap, fullNameContactMap);
                if (!match.IsNullOrEmpty())
                {
                    Log("Found matching record in system for contact: {0}", d);
                    Log("Please double check in case of a false match.");
                    continue;
                }

                var contact = new Entity("contact");
                var emailKey1 = d.PrimaryEmail.ToKey(true);
                var emailKey2 = d.EmailForStudents.ToKey(true);
                var nameKey = d.FullName.ToKey(false);

                contact["firstname"] = d.FirstName.Trim();
                contact["lastname"] = d.LastName.Trim();
                contact["middlename"] = d.MiddleName.TrimOrEmpty();
                contact["new_chinesename"] = d.ChineseName.TrimOrEmpty();
                contact["description"] = string.Format("Imported by CRM utility at {0}", DateTime.Now);
                contact["customertypecode"] = contactType;
                contact["emailaddress1"] = emailKey1.TrimOrEmpty();
                contact["emailaddress2"] = emailKey2.TrimOrEmpty();

                if (Address.IsFilled(d.Address))
                {
                    contact["address1_line1"] = d.Address.AddressLine1;
                    contact["address1_line2"] = d.Address.AddressLine2;
                    contact["address1_city"] = d.Address.City;
                    contact["address1_stateorprovince"] = d.Address.State;
                    contact["address1_postalcode"] = d.Address.Zip;
                }

                var id = service.Create(contact);
                var entityRef = new EntityReferenceCollection { new EntityReference("contact", id) };

                if (!string.IsNullOrEmpty(nameKey) && fullNameContactMap != null)
                {
                    fullNameContactMap.Add(nameKey, entityRef);
                }

                if (!string.IsNullOrEmpty(emailKey1))
                {
                    emailContactMap.Add(emailKey1, entityRef);
                }

                if (!string.IsNullOrEmpty(emailKey2) && !emailContactMap.ContainsKey(emailKey2))
                {
                    emailContactMap.Add(emailKey2, entityRef);
                }

                createdCount++;
                Log("Successfully created contact: name = {0}, id = {1}", d.FullName, id);
            }

            Log("Contacts processed: {0}, created: {1}", processedCount, createdCount);
        }

        // Overloaded ImportContacts for students import.
        public void ImportContacts(
            IOrganizationService service,
            IEnumerable<Student> contacts,
            OptionSetValue contactType,
            IDictionary<string, EntityReferenceCollection> emailContactMap,
            IDictionary<string, EntityReferenceCollection> fullNameContactMap)
        {
            if (service == null) throw new ArgumentNullException("service");
            if (contacts == null) throw new ArgumentNullException("contacts");
            if (contactType == null) throw new ArgumentNullException("contactType");
            if (emailContactMap == null) throw new ArgumentNullException("emailContactMap");

            int processedCount = 0;
            int createdCount = 0;

            foreach (var d in contacts)
            {
                processedCount++;

                var match = FindStudent(d, emailContactMap, fullNameContactMap);
                if (!match.IsNullOrEmpty())
                {
                    Log("Found matching record in system for contact: {0}", d);
                    Log("Please double check in case of a false match.");
                    continue;
                }

                var contact = new Entity("contact");
                var emailKey1 = d.PrimaryEmail.ToKey(true);
                var nameKey = d.FullName.ToKey(false);

                // Get the first & last names from the fullname.
                var names = d.FullName.Split(' ');
                if (names.Length != 2 && names.Length != 3) throw new ArgumentException("Invalid full name");
                var firstname = names[0];
                var middlename = (names.Length == 2) ? "" : names[1];
                var lastname = (names.Length == 2) ? names[1] : names[2];

                contact["firstname"] = firstname;
                contact["lastname"] = lastname;
                contact["new_chinesename"] = d.ChineseName.TrimOrEmpty();
                contact["description"] = string.Format("Imported by CRM utility at {0}", DateTime.Now);
                contact["customertypecode"] = contactType;
                contact["emailaddress1"] = emailKey1.TrimOrEmpty();
                contact["mobilephone"] = d.Mobile;
                //contact["birthdate"] = 
                contact["websiteurl"] = d.ForumLink;

                if (Address.IsFilled(d.Address))
                {
                    contact["address1_line1"] = d.Address.AddressLine1;
                    contact["address1_line2"] = d.Address.AddressLine2;
                    contact["address1_city"] = d.Address.City;
                    contact["address1_stateorprovince"] = d.Address.State;
                    contact["address1_postalcode"] = d.Address.Zip;
                }

                var id = service.Create(contact);
                var entityRef = new EntityReferenceCollection { new EntityReference("contact", id) };

                if (!string.IsNullOrEmpty(nameKey) && fullNameContactMap != null)
                {
                    fullNameContactMap.Add(nameKey, entityRef);
                }

                if (!string.IsNullOrEmpty(emailKey1))
                {
                    emailContactMap.Add(emailKey1, entityRef);
                }

                createdCount++;
                Log("Successfully created student: name = {0}, id = {1}", d.FullName, id);
            }

            Log("Students processed: {0}, created: {1}", processedCount, createdCount);
        }

        public DataCollection<Entity> GetDonationsByDonorId(IOrganizationService service, Guid id)
        {
            var donationQuery = new QueryExpression
            {
                EntityName = "new_donation",
                ColumnSet = new ColumnSet("new_donationid", "new_date", "new_amount", "new_microsoftmatchamount", "new_additionalinfo", "new_type"),
                Criteria = new FilterExpression
                {
                    Conditions = 
                    { 
                        new ConditionExpression("new_donor", ConditionOperator.Equal, id)
                    }
                }
            };

            var req = new RetrieveMultipleRequest
            {
                Query = donationQuery
            };

            var resp = (RetrieveMultipleResponse)service.Execute(req);

            return resp.EntityCollection.Entities;
        }

        public OptionSetValue GetOptionSetValueByContacType(string contactType)
        {
            if (string.IsNullOrWhiteSpace(contactType))
            {
                throw new ArgumentException("A valid contact type is required.", "contactType");
            }

            contactType = contactType.ToKey(true);
            switch (contactType)
            {
                case "student":
                    return new OptionSetValue(100000000);
                case "donor":
                    return new OptionSetValue(100000001);
                case "teacher":
                    return new OptionSetValue(100000002);
                case "volunteer":
                    return new OptionSetValue(100000003);
                default:
                    throw new ArgumentException("Invalid contact type: " + contactType, "contactType");
            }
        }

        public EntityReferenceCollection GetCurrency(IOrganizationService service, string currencyCode)
        {
            if (service == null)
            {
                throw new ArgumentNullException("service");
            }

            if (string.IsNullOrWhiteSpace(currencyCode))
            {
                throw new ArgumentException("Invalid currency code.", "currencyCode");
            }

            if (currencyMap.IsNullOrEmpty())
            {
                BuildCurrencyMap(service);
            }

            var key = currencyCode.ToKey(true);
            if (currencyMap != null && currencyMap.ContainsKey(key))
            {
                return currencyMap[key];
            }

            return null;
        }

        private void BuildCurrencyMap(IOrganizationService service)
        {
            currencyMap = new Dictionary<string, EntityReferenceCollection>();
            using (var orgSvcContext = new OrganizationServiceContext(service))
            {
                var currencies = orgSvcContext.CreateQuery("transactioncurrency").ToArray();
                foreach (var c in currencies)
                {
                    var tc = c as TransactionCurrency;
                    if (tc == null) continue;

                    var key = tc.ISOCurrencyCode.ToKey(true);
                    if (currencyMap.ContainsKey(key)) continue;

                    currencyMap.Add(key, new EntityReferenceCollection { new EntityReference("transactioncurrency", tc.Id) });
                    Log("Currency loaded: {0}", tc.ISOCurrencyCode);
                }
            }
        }

        private void Log(string format, params object[] args)
        {
            logger.Log(format, args);
        }

        private void PopulateContactByName(string name, Guid id, IDictionary<string, EntityReferenceCollection> target)
        {
            if (string.IsNullOrWhiteSpace(name))
            {
                return;
            }

            name = name.ToKey(false);
            if (!target.ContainsKey(name))
            {
                target[name] = new EntityReferenceCollection();
            }

            target[name].Add(new EntityReference("contact", id));

            if (1 < target[name].Count)
            {
                Log("Duplicate name: {0}", name);
            }
        }

        private void PopulateContactByEmail(string email, Guid id, IDictionary<string, EntityReferenceCollection> target)
        {
            if (string.IsNullOrWhiteSpace(email))
            {
                return;
            }

            email = email.ToKey(true);
            if (!target.ContainsKey(email))
            {
                target[email] = new EntityReferenceCollection();
            }

            if (target[email].Any(e => e.Id == id))
            {
                return; // DO not add a new reference if the same contact id already exists
            }

            target[email].Add(new EntityReference("contact", id));

            if (1 < target[email].Count)
            {
                Log("Identical email for multiple contacts detected: {0}", email);
            }
        }
    }
}
